// Copyright 2023 Red Hat, Inc. and/or its affiliates
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package platform

import (
	"context"
	"regexp"
	"runtime"
	"strings"
	"time"

	corev1 "k8s.io/api/core/v1"
	k8serrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/klog/v2"
	ctrl "sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/yaml"

	"github.com/kiegroup/kogito-serverless-operator/controllers/workflowdef"

	"github.com/kiegroup/kogito-serverless-operator/container-builder/client"
	"github.com/kiegroup/kogito-serverless-operator/container-builder/util/defaults"
	"github.com/kiegroup/kogito-serverless-operator/log"

	operatorapi "github.com/kiegroup/kogito-serverless-operator/api/v1alpha08"
)

var builderDockerfileFromRE = regexp.MustCompile(`FROM (.*) AS builder`)

// ResourceCustomizer can be used to inject code that changes the objects before they are created.
type ResourceCustomizer func(object ctrl.Object) ctrl.Object

func ConfigureRegistry(ctx context.Context, c client.Client, p *operatorapi.SonataFlowPlatform, verbose bool) error {
	if p.Spec.Build.Config.BuildStrategy == operatorapi.PlatformBuildStrategy && p.Status.Cluster == operatorapi.PlatformClusterOpenShift {
		p.Spec.Build.Config.Registry = operatorapi.RegistrySpec{}
		klog.V(log.D).InfoS("Platform registry not set and ignored on openshift cluster")
		return nil
	}

	if p.Spec.Build.Config.Registry.Address == "" && p.Status.Cluster == operatorapi.PlatformClusterKubernetes {
		// try KEP-1755
		address, err := GetRegistryAddress(ctx, c)
		if err != nil && verbose {
			klog.V(log.E).ErrorS(err, "Cannot find a registry where to push images via KEP-1755")
		} else if err == nil && address != nil {
			p.Spec.Build.Config.Registry.Address = *address
		}
	}

	klog.V(log.D).InfoS("Final Registry Address", "address", p.Spec.Build.Config.Registry.Address)
	return nil
}

func SetPlatformDefaults(p *operatorapi.SonataFlowPlatform, verbose bool) error {
	if p.Spec.Build.Config.BuildStrategyOptions == nil {
		klog.V(log.D).InfoS("SonataFlow Platform: setting publish strategy options", "namespace", p.Namespace)
		p.Spec.Build.Config.BuildStrategyOptions = map[string]string{}
	}

	if p.Spec.Build.Config.GetTimeout().Duration != 0 {
		d := p.Spec.Build.Config.GetTimeout().Duration.Truncate(time.Second)

		if verbose && p.Spec.Build.Config.Timeout.Duration != d {
			klog.V(log.I).InfoS("ContainerBuild timeout minimum unit is sec", "configured", p.Spec.Build.Config.GetTimeout().Duration, "truncated", d)
		}

		klog.V(log.D).InfoS("SonataFlow Platform: setting build timeout", "namespace", p.Namespace)
		p.Spec.Build.Config.Timeout = &metav1.Duration{
			Duration: d,
		}
	} else {
		klog.V(log.D).InfoS("SonataFlow Platform setting default build timeout to 5 minutes", "namespace", p.Namespace)
		p.Spec.Build.Config.Timeout = &metav1.Duration{
			Duration: 5 * time.Minute,
		}
	}

	if p.Spec.Build.Config.IsStrategyOptionEnabled(kanikoBuildCacheEnabled) {
		p.Spec.Build.Config.BuildStrategyOptions[kanikoPVCName] = p.Name
		if len(p.Spec.Build.Config.BaseImage) == 0 {
			p.Spec.Build.Config.BaseImage = workflowdef.GetDefaultWorkflowBuilderImageTag()
		}
	}

	if p.Spec.Build.Config.BuildStrategy == operatorapi.OperatorBuildStrategy && !p.Spec.Build.Config.IsStrategyOptionEnabled(kanikoBuildCacheEnabled) {
		// Default to disabling Kaniko cache warmer
		// Using the cache warmer pod seems unreliable with the current Kaniko version
		// and requires relying on a persistent volume.
		defaultKanikoBuildCache := "false"
		p.Spec.Build.Config.BuildStrategyOptions[kanikoBuildCacheEnabled] = defaultKanikoBuildCache
		if verbose {
			klog.V(log.I).InfoS("Kaniko cache set", "value", defaultKanikoBuildCache)
		}
	}

	setStatusAdditionalInfo(p)

	if verbose {
		klog.V(log.I).InfoS("baseImage set", "value", p.Spec.Build.Config.BaseImage)
		klog.V(log.I).InfoS("Timeout set", "value", p.Spec.Build.Config.GetTimeout())
	}
	return nil
}

func setStatusAdditionalInfo(platform *operatorapi.SonataFlowPlatform) {
	platform.Status.Info = make(map[string]string)

	klog.V(log.D).InfoS("SonataFlow Platform setting build publish strategy", "namespace", platform.Namespace)
	if platform.Spec.Build.Config.BuildStrategy == operatorapi.OperatorBuildStrategy {
		platform.Status.Info["kanikoVersion"] = defaults.KanikoVersion
	}
	klog.V(log.D).InfoS("SonataFlow setting status info", "namespace", platform.Namespace)
	platform.Status.Info["goVersion"] = runtime.Version()
	platform.Status.Info["goOS"] = runtime.GOOS
}

// GetRegistryAddress KEP-1755
// https://github.com/kubernetes/enhancements/tree/master/keps/sig-cluster-lifecycle/generic/1755-communicating-a-local-registry
func GetRegistryAddress(ctx context.Context, c client.Client) (*string, error) {
	config := corev1.ConfigMap{}
	err := c.Get(ctx, ctrl.ObjectKey{Namespace: "kube-public", Name: "local-registry-hosting"}, &config)
	if err != nil {
		if k8serrors.IsNotFound(err) {
			return nil, nil
		}
		return nil, err
	}
	if data, ok := config.Data["localRegistryHosting.v1"]; ok {
		result := LocalRegistryHostingV1{}
		if err := yaml.Unmarshal([]byte(data), &result); err != nil {
			return nil, err
		}
		return &result.HostFromClusterNetwork, nil
	}
	return nil, nil
}

// GetCustomizedDockerfile gets the Dockerfile as defined in the default platform ConfigMap, apply any custom requirements and return.
func GetCustomizedDockerfile(dockerfile string, platform operatorapi.SonataFlowPlatform) string {
	if platform.Spec.Build.Config.BaseImage != "" {
		res := builderDockerfileFromRE.FindAllStringSubmatch(dockerfile, 1)
		dockerfile = strings.Replace(dockerfile, strings.Trim(res[0][1], " "), platform.Spec.Build.Config.BaseImage, 1)
	}
	return dockerfile
}
